import sys
import pygame

from bullet import Bullet
from settings import Settings
from ship import Ship
from alien import Alien
from game_stats import GameStats
from time import sleep
from button import Button
from scoreboard import Scoreboard

class AlienInvasion:
    """
    管理游戏资源和行为的类
    """

    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()
        """
        创建一个显示窗口，游戏的所有图形元素都将在其中绘制
        实参(1200, 800)是一个元组，指定了游戏窗口的尺寸——宽1200像素、高800像素（你可以根据自己的显示器尺寸调整这些值）。
        将这个显示窗口赋给属性self.screen，让这个类中的所有方法都能够使用它。
        赋给属性self.screen的对象是一个surface。
        在Pygame中，surface是屏幕的一部分，用于显示游戏元素。
        在这个游戏中，每个元素（如外星人或飞船）都是一个surface。
        display.set_mode()返回的surface表示整个游戏窗口。
        激活游戏的动画循环后，每经过一次循环都将自动重绘这个surface，
        将用户输入触发的所有变化都反映出来。
        """
        # self.screen = pygame.display.set_mode((800, 600))
        self.settings = Settings()
        # self.screen = pygame.display.set_mode((
        #     self.settings.screen_width,
        #     self.settings.screen_height
        # ))
        self.screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
        # 使用屏幕的rect的属性width和height来更新对象settings。
        # 在全屏模式下运行这款游戏之前，请确认能够按Q键退出，因为Pygame默认不提供在全屏模式下退出游戏的方式。
        self.settings.screen_width = self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height

        # 设置当前窗口标题（游戏标题）
        pygame.display.set_caption("外星人入侵")

        # 创建一个用于存储游戏统计信息的实例
        self.stats = GameStats(self)
        # 创建记分牌
        self.sb = Scoreboard(self)
        """
            导入Ship类，并在创建屏幕后创建一个Ship实例
            调用Ship()时，必须提供一个参数：一个AlienInvasion实例。
            在这里，self指向的是当前AlienInvasion实例。
            这个参数让Ship能够访问游戏资源，如对象screen。我们将这个Ship实例赋给了self.ship。
        """
        self.ship = Ship(self)
        # 设置背景颜色
        # self.bg_color = (230, 230, 230)
        # 创建用于存储子弹的编组
        self.bullets = pygame.sprite.Group()
        self.aliens = pygame.sprite.Group()

        self._create_fleet()

        # 创建play按钮
        self.play_button = Button(self, "Play")


    def run_game(self):
        """开始游戏"""
        while True:
            """
            监视键盘和鼠标的事件
            事件是用户玩游戏时执行的操作，如按键或移动鼠标。
            为程序响应事件，可编写一个事件循环，
            以侦听事件并根据发生的事件类型执行合适的任务。
            pygame.event.get()返回一个列表，其中包含它在上一次被调用后发生的所有事件
            所有键盘和鼠标事件都将导致这个for循环运行。
            在这个循环中，我们将编写一系列if语句来检测并响应特定的事件。
            例如，当玩家单击游戏窗口的关闭按钮时，将检测到pygame.QUIT事件，
            进而调用sys.exit()来退出游戏
            """

            """
            方法update()通过Ship实例来调用
            飞船的位置将在检测到键盘事件后（但在更新屏幕前）更新。
            这样，玩家输入时，飞船的位置将更新，从而确保使用更新后的位置将飞船绘制到屏幕上。
            """
            self._check_events()
            if self.stats.game_active:
                self.ship.update()
                self._update_bullets()
                self._update_aliens()
            self._update_screen()

    def _check_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()  # 退出
            elif event.type == pygame.KEYDOWN:
                self._check_keydown_events(event)
            elif event.type == pygame.KEYUP:
                self._check_keyup_events(event)
            elif event.type == pygame.MOUSEBUTTONDOWN:
                # 无论玩家单击屏幕的什么地方，Pygame都将检测到一个MOUSEBUTTONDOWN事件
                # 我们只想让这个游戏在玩家用鼠标单击Play按钮时做出响应，
                # 为此，使用了pygame.mouse.get_pos()，它返回一个元组，其中包含玩家单击时鼠标的x坐标和y坐标
                mouse_pos = pygame.mouse.get_pos()
                # 我们将这些值传递给新方法_check_play_button()，放在_check_events()后面
                self._check_play_button(mouse_pos)

    def _check_play_button(self, mouse_pos):
        # 玩家单击play的时候，开始游戏
        # 使用rect的方法collidepoint()检查鼠标单击位置是否在Play按钮的rect内
        # 标志button_clicked的值为True或False
        button_click = self.play_button.rect.collidepoint(mouse_pos)
        # 仅当玩家单击了Play按钮且游戏当前处于非活动状态时，游戏才重新开始
        if button_click and not self.stats.game_active:
            # 重置游戏设置
            self.settings.initialize_dynamic_settings()
            # 重置游戏统计信息，给玩家提供三艘新飞船
            self.stats.reset_stats()
            self.stats.game_active = True

            self.sb.prep_score()
            self.sb.prep_level()
            self.sb.prep_ships()
            # 清空剩余的外星人和子弹
            self.aliens.empty()
            self.bullets.empty()

            # 创建新的外星人并让飞船居中
            self._create_fleet()
            self.ship.center_ship()

            # 隐藏鼠标光标
            pygame.mouse.set_visible(False)

    def _check_keydown_events(self, event):
        # 检查按下键（event.key）是否是右箭头键（pygame.K_RIGHT），从而将飞船向右移动
        if event.key == pygame.K_RIGHT:
            # 玩家按下右箭头键（K_RIGHT）时，将moving_right设置为True
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True
        elif event.key == pygame.K_q:
            sys.exit()
        # 在玩家按空格键时调用_fire_bullet()
        elif event.key == pygame.K_SPACE:
            self._fire_bullet()

    def _check_keyup_events(self, event):
        if event.key == pygame.K_RIGHT:
            # 玩家松开右箭头键（K_RIGHT）时，将moving_right设置为False
            self.ship.moving_right = False
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = False

    def _check_aliens_bottom(self):
        # 检查是否有外星人达到了屏幕底端
        screen_rect = self.screen.get_rect()
        for alien in self.aliens.sprites():
            # 到达屏幕底端后，外星人的属性rect.bottom大于或等于屏幕的属性rect.bottom
            if alien.rect.bottom >= screen_rect.bottom:
                # 与飞船被撞到一样处理
                self._ship_hit()
                # 只要检测到一个外星人到达屏幕底端，
                # 就无须检查其他外星人了，因此在调用_ship_hit()后退出循环。
                break

    def _fire_bullet(self):
        """
        创建一个Bullet实例并将其赋给new_bullet，并将他加入到bullets编组中
        玩家按空格键时，我们检查bullets的长度。
        如果len(bullets)小于3，就创建一颗新子弹；
        但如果有三颗未消失的子弹，则玩家按空格键时什么都不会发生。
        :return:
        """
        if len(self.bullets) < self.settings.bullet_allowed:
            new_bullet = Bullet(self)
            self.bullets.add(new_bullet)

    def _update_bullets(self):
        # 对编组调用update()时，编组自动对其中的每个精灵调用update()。
        # 因此代码行bullets.update()将为编组bullets中的每颗子弹调用bullet.update()。
        self.bullets.update()
        """
            删除消失的子弹
            使用for循环遍历列表（或Pygame编组）时，Python要求该列表的长度在整个循环中保持不变。
            因为不能从for循环遍历的列表或编组中删除元素，所以必须遍历编组的副本。
            我们使用方法copy()来设置for循环，从而能够在循环中修改bullets。
            我们检查每颗子弹，看看它是否从屏幕顶端消失，如果是，就将其从bullets中删除
            使用函数调用print()显示当前还有多少颗子弹，以核实确实删除了消失的子弹。
        """
        for bullet in self.bullets.copy():
            if bullet.rect.bottom <= 0:
                self.bullets.remove(bullet)
        # print(len(self.bullets))
        self._check_bullet_alien_cillisions()

    def _check_bullet_alien_cillisions(self):
        # 检查是否有子弹击中了外星人
        # 如果True，就删除子弹和外星人
        collisions = pygame.sprite.groupcollide(self.bullets, self.aliens, True, True)
        #　有子弹击中外星人时，Pygame返回一个字典（collisions）
        # 我们检查这个字典是否存在，如果存在，就将得分加上一个外星人的分数
        if collisions:
            for aliens in collisions.values():
                self.stats.score += self.settings.alien_points * len(aliens)
            # 调用prep_score()来创建一幅包含最新得分的新图像
            self.sb.prep_score()
            self.sb.check_high_score()
        # 检查编组aliens是否为空，空编组相当于False
        # 如果编组aliens为空，就使用方法empty()删除编组中余下的所有精灵，从而删除现有的所有子弹
        if not self.aliens:
            # 删除现有的子弹并新建一群外星人
            self.bullets.empty()
            # 在屏幕上重新显示一群外星人
            self._create_fleet()
            self.settings.increase_speed()

            # 提高等级
            self.stats.level += 1
            self.sb.prep_level()

    def _update_aliens(self):
        self._check_fleet_edges()
        # 更新外星人群中所有外星人位置
        self.aliens.update()

        # 检测外星人和飞船之间的碰撞
        # 函数spritecollideany()接受两个实参：一个精灵和一个编组。
        # 它检查编组是否有成员与精灵发生了碰撞，并在找到与精灵发生碰撞的成员后停止遍历编组。
        # 在这里，它遍历编组aliens，并返回找到的第一个与飞船发生碰撞的外星人。
        # 如果没有发生碰撞，spritecollideany()将返回None，
        # 如果找到了与飞船发生碰撞的外星人，它就返回这个外星人，因此if代码块将执行print语句
        if pygame.sprite.spritecollideany(self.ship,self.aliens):
            # 外星人与飞船相撞后的一系列操作
            # print('相撞了！')
            self._ship_hit()
        # 检查是否有外星人达到了底端
        self._check_aliens_bottom()

    def _create_fleet(self):
        """ 创建外星人群 """
        # 创建了一个Alien实例，再将其添加到用于存储外星人群的编组中。
        # 为放置外星人，需要知道外星人的宽度和高度，因此在执行计算前，创建一个外星人
        # 这个外星人不是外星人群的成员，因此没有将其加入编组aliens中
        alien = Alien(self)
        # alien_width = alien.rect.width
        # size属性是一个元组，包含rect对象的宽度和高度
        alien_width,alien_height = alien.rect.size
        # 计算可用于放置外星人的水平空间以及其中可容纳多少个外星人。
        available_space_x = self.settings.screen_width - (2 * alien_width)
        number_aliens_x = available_space_x // (2 * alien_width)
        # 计算屏幕可容纳多少行外星人
        ship_height = self.ship.rect.height
        # 计算屏幕可容纳多少行外星人
        available_space_y = (self.settings.screen_height - (3 * alien_height) -ship_height)
        number_rows = available_space_y // (2 * alien_height)
        # 创建外星人群
        # 内部循环创建一行外星人，而外部循环从零数到要创建的外星人行数：
        # Python将重复执行创建单行外星人的代码，重复次数为number_rows。
        for row_number in range(number_rows):
            # 创建第一个外星人,从零数到要创建的外星人数
            for alien_number in range(number_aliens_x):
                # 创建一个外星人并将其加入当前列
                self._create_alien(alien_number, row_number)

    def _create_alien(self, alien_number, row_number):
        alien = Alien(self)
        alien_width, alien_height = alien.rect.size
        alien.x = alien_width + 2 * alien_width * alien_number
        alien.rect.x = alien.x
        # 在第一行外星人上方留出与外星人等高的空白区域
        # 相邻外星人行的y坐标相差外星人高度的两倍，因此将外星人高度乘以2，再乘以行号。
        # 第一行的行号为0，因此第一行的垂直位置不变，而其他行都沿屏幕依次向下放置。
        alien.rect.y = alien.rect.height + 2 * alien_height * row_number
        self.aliens.add(alien)

    def _check_fleet_edges(self):
        # 当有外星人到达边缘的时候采取响应的措施
        for alien in self.aliens.sprites():
            # 遍历外星人群并对其中的每个外星人调用check_edges()
            if alien.check_edges():
                # 如果check_edges()返回True，就表明相应的外星人位于屏幕边缘，
                # 需要改变外星人群的方向，因此调用_change_fleet_direction()并退出循环
                self._change_fleet_direction()
                break

    def _change_fleet_direction(self):
        # 将整群外星人下移并改变他们的方向
        for alien in self.aliens.sprites():
            # 遍历所有外星人，将每个外星人下移设置fleet_drop_speed的值
            alien.rect.y += self.settings.fleet_drop_speed
        # 将fleet_direction的值改为其当前值与-1的乘积。
        # 调整外星人群移动方向的代码行没有包含在for循环中，
        # 因为我们要调整每个外星人的垂直位置，但只想调整外星人群移动方向一次。
        self.settings.fleet_direction *= -1

    def _update_screen(self):
        # 每次循环的时候都会重新绘制屏幕
        # 调用方法fill()用这种背景色填充屏幕。
        # 方法fill()用于处理surface，只接受一个实参：一种颜色。
        self.screen.fill(self.settings.bg_color)
        # 填充背景后，调用ship.blitme()将飞船绘制到屏幕上，确保它出现在背景前面
        self.ship.blitme()

        """
        方法bullets.sprites()返回一个列表，其中包含编组bullets中的所有精灵。
        为在屏幕上绘制发射的所有子弹，遍历编组bullets中的精灵，并对每个精灵调用draw_bullet()
        """
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()

        # 让外星人现身，对编组调用draw()时，
        # Pygame将把编组中的每个元素绘制到属性rect指定的位置。
        # 方法draw()接受一个参数，这个参数指定了要将编组中的元素绘制到哪个surface上。
        self.aliens.draw(self.screen)
        # 显示得分
        self.sb.show_score()
        # 如果游戏处于非活动状态就绘制play按钮
        if not self.stats.game_active:
            self.play_button.draw_button()

        """
                    pygame.display.flip()，命令Pygame让最近绘制的屏幕可见
                    在这里，它在每次执行while循环时都绘制一个空屏幕，并擦去旧屏幕，
                    使得只有新屏幕可见。我们移动游戏元素时，
                    pygame.display.flip()将不断更新屏幕，以显示元素的新位置，
                    并且在原来的位置隐藏元素，从而营造平滑移动的效果。
                    注意：一定要写在while循环里
                    """
        pygame.display.flip()

    def _ship_hit(self):
        # 响应飞船被外星人撞到
        if self.stats.ship_left > 0:
            # 1、将ship_left减去1
            self.stats.ship_left -= 1
            self.sb.prep_ships()
            # 2、清空余下的外星人和子弹
            self.aliens.empty()
            self.bullets.empty()
            # 3、创建一群新的外星人，并将飞船放到屏幕的底端的中央
            self._create_fleet()
            self.ship.center_ship() # 稍后将在Ship类中添加方法center_ship()
            # 4、暂停,让玩家知道飞船被撞到了
            sleep(0.5)
            # 函数sleep()执行完毕后，将接着执行方法_update_screen()，将新的外星人群绘制到屏幕上。
        else:
            self.stats.game_active = False
            pygame.mouse.set_visible(True)

"""
一个python文件通常有两种使用方法，
第一是作为脚本直接执行，
第二是 import 到其他的 python 脚本中被调用（模块重用）执行。
仅当直接运行该文件时，它们才会执行，而 import 到其他脚本中是不会被执行的
"""
if __name__ == "__main__":
    ai = AlienInvasion()
    ai.run_game()
